
// Copyright (C) 2009-2011 Luca Piccioni
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//  
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//  
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Windows.Forms;

using Derm.OpenGL;
using Derm.Raster;

namespace Derm.Render
{
	/// <summary>
	/// Rendering window.
	/// </summary>
	/// <remarks>
	/// <para>
	/// A RenderWindow shall be considered an object able to output the graphical result
	/// of some rendering operation. A RenderWindow instance must have associated a RenderContext
	/// instance.
	/// </para>
	/// </remarks>
	public class RenderWindow : RenderSurface
	{
		#region Constructors

		/// <summary>
		/// Construct a RenderWindow.
		/// </summary>
		public RenderWindow() : this(0, 0) { }

		/// <summary>
		/// Construct a RenderWindow specifying its extents.
		/// </summary>
		/// <param name="w">
		/// A <see cref="System.Int32"/> specifing window width (in pixels).
		/// </param>
		/// <param name="h">
		/// A <see cref="System.Int32"/> specifing window height (in pixels).
		/// </param>
		public RenderWindow(uint w, uint h) : base(w, h)
		{
			// Create Form linked with this RenderWindow
			mRenderControl = mRenderForm = new RenderForm(this);
			// Obtain device context (relative to window)
			mDeviceContext = DeviceContextFactory.Create(mRenderForm);
		}

		/// <summary>
		/// Construct a RenderWindow bound to a Control.
		/// </summary>
		/// <param name="control">
		/// A <see cref="Control"/> which is used for creating a OpenGL surface.
		/// </param>
		public RenderWindow(Control control) : base()
		{
			if (control == null)
				throw new ArgumentNullException("control");

			// No form: using directly the handle passed as argument
			mRenderControl = control;
			// Obtain device context (relative to window)
			mDeviceContext = DeviceContextFactory.Create(mRenderForm);
		}
		
		/// <summary>
		/// Destructor. 
		/// </summary>
		/// <remarks>
		/// </remarks>
		~RenderWindow()
		{
			// Release device context
			if (mDeviceContext != null) {
				mDeviceContext.Release();
				mDeviceContext = null;
			}
		}

		#endregion
		
		#region User Interface Management
		
		/// <summary>
		/// Main window property. 
		/// </summary>
		public bool MainWindow
		{
			get { return (mMainWindow); }
			set { mMainWindow = value; }
		}

		/// <summary>
		/// Make this RenderWindow visible.
		/// </summary>
		public void ShowWindow()
		{
			mRenderForm.Show();
		}

		/// <summary>
		/// Make this RenderWindow invisible.
		/// </summary>
		public void HideWindow()
		{
			mRenderForm.Hide();
		}

		/// <summary>
		/// Check whether this RenderWindow is visible.
		/// </summary>
		public bool Visible
		{
			get {
				return (mRenderForm.Visible);
			}
		}

		/// <summary>
		/// Register mouse input handler.
		/// </summary>s
		/// <param name="input"></param>
		//public void RegisterInput(MouseInput input)
		//{
		//    mRenderForm.MouseDown += new MouseEventHandler(input.MouseDownHandler);
		//    mRenderForm.MouseUp += new MouseEventHandler(input.MouseUpHandler);
		//    mRenderForm.MouseMove += new MouseEventHandler(input.MouseMoveHandler);
		//    mRenderForm.MouseWheel += new MouseEventHandler(input.MouseWheelHandler);
		//}

		///// <summary>
		///// Register keyboard input handler.
		///// </summary>
		///// <param name="input"></param>
		//public void RegisterInput(IKeyboardInput input)
		//{
		//    mRenderForm.KeyDown += new KeyEventHandler(input.KeyDownEvent);
		//    mRenderForm.KeyUp += new KeyEventHandler(input.KeyUpEvent);
		//}
		
		/// <summary>
		/// Main window flag. 
		/// </summary>
		private bool mMainWindow;
		
		#endregion
		
		#region Nested Form Management
		
		/// <summary>
		/// Form used for rendering. 
		/// </summary>
		private class RenderForm : Form
		{
			/// <summary>
			/// RenderForm constructor. 
			/// </summary>
			public RenderForm(RenderWindow win)
			{
				if (win == null)
					throw new ArgumentNullException("win");

				// Store RenderWindow reference
				mWindow = win;
				// Initialize Form extents
				SetClientSizeCore((int)win.Width, (int)win.Height);
				
				// No need to erase window background
				SetStyle(ControlStyles.AllPaintingInWmPaint, true);
				// No need to draw window background
				SetStyle(ControlStyles.Opaque, true);
				// Buffer control
				SetStyle(ControlStyles.DoubleBuffer, true);
				// Redraw window on resize
				SetStyle(ControlStyles.ResizeRedraw, true);
				// Painting handled by user
				SetStyle(ControlStyles.UserPaint, true);
				
				// React on window resize event
				Resize += new EventHandler(mWindow.FormResize);
				// React on window closed event
				FormClosed += new FormClosedEventHandler(mWindow.FormClosed);
			}
			
			/// <summary>
			/// RenderWindow linked with this Form. 
			/// </summary>
			private RenderWindow mWindow;
		}

		/// <summary>
		/// Set this RenderWindow position.
		/// </summary>
		/// <param name="x">
		/// A <see cref="System.Int32"/> that specifies the X coordinate.
		/// </param>
		/// <param name="y">
		/// A <see cref="System.Int32"/> that specifies the Y coordinate.
		/// </param>
		public void SetPosition(int x, int y)
		{
			mRenderForm.Location = new System.Drawing.Point(x, y);
		}

		/// <summary>
		/// Get this RenderWindow position.
		/// </summary>
		/// <param name="x">
		/// A <see cref="System.Int32"/> returning the X coordinate.
		/// </param>
		/// <param name="y">
		/// A <see cref="System.Int32"/> returning the Y coordinate.
		/// </param>
		public void GetPosition(out int x, out int y)
		{
			x = mRenderForm.Location.X;
			y = mRenderForm.Location.Y;
		}

		/// <summary>
		/// Set this RenderWindow extents.
		/// </summary>
		/// <param name="w">
		/// A <see cref="System.Int32"/> that specifies the RenderWindow width.
		/// </param>
		/// <param name="h">
		/// A <see cref="System.Int32"/> that specifies the RenderWindow height.
		/// </param>
		public void SetExtents(uint w, uint h)
		{
			mRenderForm.ClientSize = new System.Drawing.Size((int)w, (int)h);
		}

		/// <summary>
		/// Get this RenderWindow extents.
		/// </summary>
		/// <param name="w">
		/// A <see cref="System.Int32"/> returning the RenderWindow width.
		/// </param>
		/// <param name="h">
		/// A <see cref="System.Int32"/> returning the RenderWindow height.
		/// </param>
		public void GetExtents(out uint w, out uint h)
		{
			w = (uint)mRenderForm.ClientSize.Width;
			h = (uint)mRenderForm.ClientSize.Height;
		}

		/// <summary>
		/// Get the Form handle used for rendering.
		/// </summary>
		public Form NestedForm
		{
			get { return (mRenderForm); }
		}

		/// <summary>
		/// Form where rendering results are drawn. 
		/// </summary>
		private readonly RenderForm mRenderForm;

		/// <summary>
		/// Control where rendering result are drawn.
		/// </summary>
		private Control mRenderControl;
		
		#endregion
		
		#region Form Event Handlers
		
		/// <summary>
		///  
		/// </summary>
		/// <param name="sender">
		/// A <see cref="System.Object"/>
		/// </param>
		/// <param name="e">
		/// A <see cref="EventArgs"/>
		/// </param>
		private void FormResize(object sender, EventArgs e)
		{
			Form control = (Form)sender;
			
			// Store new form extents
			Height = (uint)control.ClientSize.Height;
			Width = (uint)control.ClientSize.Width;
		}
		
		/// <summary>
		/// Form closed event handler.
		/// </summary>
		/// <param name="sender">
		/// A <see cref="System.Object"/> which has raised the event.
		/// </param>
		/// <param name="e">
		/// A <see cref="EventArgs"/> that specifies the event parameters.
		/// </param>
		/// <remarks>
		/// In the case this RenderWindow has the property MainWindow as true, it closes
		/// the RenderKernel main thread and exit from the application.
		/// </remarks>
		private void FormClosed(object sender, EventArgs e)
		{
			if (MainWindow == true) {
				RenderKernel.StopKernels();
				Application.Exit();
			}
		}

		#endregion

		#region Form Fullscreen Management

		/// <summary>
		/// Fullscreen window property.
		/// </summary>
		public bool Fullscreen
		{
			get {
				return (mFullscreen);
			}
			set {
				if (value == true) {
					SetFullscreen();
				} else {
					ResetFullscreen();
				}
			}
		}

		/// <summary>
		/// Set this RenderWindow fullscreen, keeping the position and size.
		/// </summary>
		public void SetFullscreen()
		{
			// No border
			mRenderForm.FormBorderStyle = FormBorderStyle.None;
			// Maximized
			mRenderForm.WindowState = FormWindowState.Maximized;
			// Top most
			mRenderForm.TopMost = true;
			// Cursor hidden
			Cursor.Hide();

			// Keep track of fullscreen state
			mFullscreen = true;
		}

		/// <summary>
		/// Set the window into fullscreen mode, bounds to a screen position and size.
		/// </summary>
		/// <param name="screen">
		/// A <see cref="Screen"/> that specifies the position and dimension of the RenderWindow.
		/// </param>
		public void SetFullscreen(Screen screen)
		{
			// The window is bound to a screen
			mRenderForm.DesktopBounds = screen.Bounds;
			// The window is fullscren
			SetFullscreen();
		}

		/// <summary>
		/// Reset this RenderWindow fullscreen state.
		/// </summary>
		public void ResetFullscreen()
		{
			// With border
			mRenderForm.FormBorderStyle = FormBorderStyle.Sizable;
			// Maximized
			mRenderForm.WindowState = FormWindowState.Normal;
			// Not top most
			mRenderForm.TopMost = false;
			// Cursor visible
			Cursor.Show();

			// Keep track of fullscreen state
			mFullscreen = false;
		}
		
		/// <summary>
		/// RenderWindow fullscreen flag.
		/// </summary>
		private bool mFullscreen;
		
		#endregion

		#region RenderWindow Creation

		/// <summary>
		/// Create this RenderWindow.
		/// </summary>
		/// <param name="surfaceFormat"></param>
		public void Create(RenderSurfaceFormat surfaceFormat)
		{
			if (surfaceFormat == null)
				throw new ArgumentNullException("surfaceFormat");

			// Store surface format
			mSurfaceFormat = surfaceFormat;
			// Actually create this RenderWindow
			Create((RenderContext)null);
		}

		#endregion

		#region Stereoscopic Windows Management

		/// <summary>
		/// The phisical position of a window in a stereoscopic context.
		/// </summary>
		public enum StereoSide
		{
			/// <summary>
			/// Left channel.
			/// </summary>
			Left,
			/// <summary>
			/// Right channel.
			/// </summary>
			Right,
		}

		/// <summary>
		/// The phisical position of this window in a stereoscopic context.
		/// </summary>
		public StereoSide Stereo
		{
			get { return (mStereoSide); }
			set {
				mStereoSide = value;
			}
		}

		/// <summary>
		/// Default value.
		/// </summary>
		private StereoSide mStereoSide = StereoSide.Left;

		#endregion

		#region Window Grabbing

		/// <summary>
		/// Read this RenderSurface color buffer.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/>
		/// </param>
		/// <param name="x">
		/// A <see cref="System.Int32"/> that specifies the x coordinate of the lower left corder of the rectangle area to read.
		/// </param>
		/// <param name="y">
		/// A <see cref="System.Int32"/> that specifies the y coordinate of the lower left corder of the rectangle area to read.
		/// </param>
		/// <param name="width">
		/// A <see cref="System.Int32"/> that specifies the width of the rectangle area to read.
		/// </param>
		/// <param name="height">
		/// A <see cref="System.Int32"/> that specifies the height of the rectangle area to read.
		/// </param>
		/// <param name="pType">
		/// A <see cref="Pixel.Type"/> which determine the pixel storage of the returned image.
		/// </param>
		/// <returns>
		/// It returns an <see cref="Image"/> representing the current read buffer <paramref name="rBuffer"/>.
		/// </returns>
		public Image ReadFrontColorBuffer(RenderContext ctx, uint x, uint y, uint width, uint height, Pixel.Type pType)
		{
			return (ReadBuffer(ctx, (Stereo == StereoSide.Left) ? Gl.FRONT_LEFT : Gl.FRONT_RIGHT, x, y, width, height, pType));
		}

		/// <summary>
		/// Read this RenderSurface color buffer.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/>
		/// </param>
		/// <param name="x">
		/// A <see cref="System.Int32"/> that specifies the x coordinate of the lower left corder of the rectangle area to read.
		/// </param>
		/// <param name="y">
		/// A <see cref="System.Int32"/> that specifies the y coordinate of the lower left corder of the rectangle area to read.
		/// </param>
		/// <param name="width">
		/// A <see cref="System.Int32"/> that specifies the width of the rectangle area to read.
		/// </param>
		/// <param name="height">
		/// A <see cref="System.Int32"/> that specifies the height of the rectangle area to read.
		/// </param>
		/// <param name="pType">
		/// A <see cref="Pixel.Type"/> which determine the pixel storage of the returned image.
		/// </param>
		/// <returns>
		/// It returns an <see cref="Image"/> representing the current read buffer <paramref name="rBuffer"/>.
		/// </returns>
		public Image ReadBackColorBuffer(RenderContext ctx, uint x, uint y, uint width, uint height, Pixel.Type pType)
		{
			return (ReadBuffer(ctx, (Stereo == StereoSide.Left) ? Gl.BACK_LEFT : Gl.BACK_RIGHT, x, y, width, height, pType));
		}

		#endregion

		#region RenderSurface Overrides

		/// <summary>
		/// Get the device context associated to this RenderWindow. 
		/// </summary>
		/// <returns>
		/// A <see cref="IDeviceContext"/> representing the device context related to this RenderWindow.
		/// </returns>
		public override IDeviceContext GetDeviceContext()
		{
			return (mDeviceContext);
		}

		/// <summary>
		/// Current surface configuration.
		/// </summary>
		/// <remarks>
		/// <para>
		/// This read-only property shall return a <see cref="RenderSurfaceFormat"/> indicating the current
		/// buffer configuration. The object returned shall not be used to modify this RenderSurface buffers,
		/// but it shall be used to know which is the buffer configuration.
		/// </para>
		/// </remarks>
		public override RenderSurfaceFormat BufferFormat { get { return ((RenderSurfaceFormat)mSurfaceFormat.Clone()); } }

		/// <summary>
		/// Bind this RenderSurface for drawing.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> to wich associate its rendering result to this RenderSurface.
		/// </param>
		public override void BindDraw(RenderContext ctx)
		{
			// Eventually unbind a framebuffer
			if (ctx.Caps.FrambufferObject.Supported == true) {
				Gl.BindFramebuffer(Gl.DRAW_FRAMEBUFFER, InvalidObjectName);
				Debug.Assert(RenderException.CheckErrors() == true);
			}
		}

		/// <summary>
		/// Bind this RenderSurface for reading.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> to wich associate its read result to this RenderSurface.
		/// </param>
		public override void BindRead(RenderContext ctx)
		{
			// Eventually unbind a framebuffer
			if (ctx.Caps.FrambufferObject.Supported == true) {
				Gl.BindFramebuffer(Gl.READ_FRAMEBUFFER, InvalidObjectName);
				Debug.Assert(RenderException.CheckErrors() == true);
			}
		}

		/// <summary>
		/// Determine whether this surface has to be swapped.
		/// </summary>
		public override bool Swappable
		{
			get { return (mSurfaceFormat.HasBuffer(RenderSurfaceFormat.BufferType.Double)); }
		}

		/// <summary>
		/// Swap render surface. 
		/// </summary>
		public override void SwapSurface()
		{
			if (mDeviceContext == null)
				throw new InvalidOperationException();
			
			if (mSurfaceFormat.HasBuffer(RenderSurfaceFormat.BufferType.Double) == true)
				Gl.SwapBuffers(mDeviceContext);
		}

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting managed/unmanaged resources.
		/// </summary>
		/// <param name="disposing">
		/// </param>
		protected override void Dispose(bool disposing)
		{
			if (disposing == true) {
				// Dispose unmanaged resources
				if (mDeviceContext != null) {
					mDeviceContext.Release();
					mDeviceContext = null;
				}
			}

			// Dispose managed resources

		}

		/// <summary>
		/// Device context associated to mRenderWindow. 
		/// </summary>
		private IDeviceContext mDeviceContext;
		
		#endregion

		#region RenderResource Overrides

		/// <summary>
		/// RenderResource object class.
		/// </summary>
		internal static readonly Guid RenderWindowObjectClass = new Guid("DE73B51E-2932-436C-BEC4-FD77203012F6");

		/// <summary>
		/// RenderResource object class.
		/// </summary>
		public override Guid ObjectClass { get { return (RenderWindowObjectClass); } }

		/// <summary>
		/// Determine whether this object requires a name bound to a context or not.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> used for creating this object name.
		/// </param>
		/// <returns>
		/// It returns a false, since this is not a real OpenGL object.
		/// </returns>
		protected override bool RequiresName(RenderContext ctx)
		{
			return (false);
		}

		/// <summary>
		/// Actually create this RenderFramebuffer resources.
		/// </summary>
		/// <param name="ctx">
		/// A <see cref="RenderContext"/> used for allocating resources.
		/// </param>
		protected override void CreateObject(RenderContext ctx)
		{
			if (ctx != null)
				throw new ArgumentException("context not required", "ctx");

			if (Environment.OSVersion.Platform == PlatformID.Win32Windows) {
				WindowsDeviceContext winDeviceContext = (WindowsDeviceContext)mDeviceContext;
				Wgl.PIXELFORMATDESCRIPTOR pDescriptor;
	
				// Choose "best" pixel format matching with surface configuration
				RenderContext.PixelFormat pFormat = mSurfaceFormat.ChoosePixelFormat(mDeviceContext, ValidPixelFormat);

				// Set choosen pixel format
				if (Wgl.SetPixelFormat(winDeviceContext.DeviceContext, pFormat.FormatIndex, out pDescriptor) == false)
					throw new InvalidOperationException("unable to select surface pixel format");
				// Confirm surface configuration
				mSurfaceFormat.SetBufferConfiguration(pFormat);
			} else {
				XServerDeviceContext x11DeviceContext = (XServerDeviceContext)mDeviceContext;

				// Choose "best" pixel format matching with surface configuration
				RenderContext.PixelFormat pFormat = mSurfaceFormat.ChoosePixelFormat(mDeviceContext, ValidPixelFormat);

				x11DeviceContext.VisualConfig = pFormat.XFbConfig;
			}
		}

		/// <summary>
		/// Derived implementation status for passing surface pixel format.
		/// </summary>
		/// <param name="pFormat">
		/// A <see cref="RenderContext.PixelFormat"/> to be validated by derived implementation.
		/// </param>
		/// <returns>
		/// It returns true in the case the pixel format is valid for usage in the derived implementation,
		/// otherwise it returns false.
		/// </returns>
		private bool ValidPixelFormat(RenderContext.PixelFormat pFormat)
		{
			return (pFormat.RenderWindow == true);
		}

		/// <summary>
		/// Window surface format.
		/// </summary>
		private RenderSurfaceFormat mSurfaceFormat = new RenderSurfaceFormat(Pixel.Type.RGB24);

		#endregion
	}
}
